package com.fg.commons.cryptography;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import javax.crypto.Cipher;
import java.io.UnsupportedEncodingException;
import java.lang.String;
import java.net.URLDecoder;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * jCryption support (www.jcryption.org) - RSA encryption
 * @author Michal Franc, Jan Novotný, FG Forrest, donated to the www.jcryption.org
 * @version $Id: jCryption.java,v 1.4 2011/05/18 07:15:32 u_novoj Exp $
 */
public class jCryption {
	private static final Pattern SPLIT_BY_WHITESPACE = Pattern.compile("\\s");
	private static final Pattern SPLIT_BY_AMPERSAND = Pattern.compile("&");
	private static final Pattern SPLIT_BY_EQUALITY = Pattern.compile("=");

	/**
     * Static Constructor
     */
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * Generates the Keypair with the given keyLength.
     *
     * @param keyLength length of key
     * @return KeyPair object
     * @throws RuntimeException if the RSA algorithm not supported
    */
    public KeyPair generateKeypair(int keyLength) {
        try {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(keyLength);
            return kpg.generateKeyPair();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("RSA algorithm not supported",e);
        }
    }

    /**
     * Decrypts a given string with the RSA keys
     * @param encrypted full encrypted text
     * @param keys RSA keys
     * @return decrypted text
     * @throws RuntimeException if the RSA algorithm not supported or decrypt operation failed
     */
    public String decrypt( String encrypted, KeyPair keys ) {
        Cipher dec;
        try {
            dec = Cipher.getInstance("RSA/NONE/NoPadding");
            dec.init(Cipher.DECRYPT_MODE, keys.getPrivate());
        } catch (GeneralSecurityException e) {
            throw new RuntimeException("RSA algorithm not supported",e);
        }
        String[] blocks = SPLIT_BY_WHITESPACE.split(encrypted);
		StringBuilder result = new StringBuilder();
        try {
            for ( int i = blocks.length-1; i >= 0; i-- ) {
                byte[] data = hexStringToByteArray(blocks[i]);
                byte[] decryptedBlock = dec.doFinal(data);
                result.append( new String(decryptedBlock) );
            }
        } catch (GeneralSecurityException e) {
            throw new RuntimeException("Decrypt error",e);
        }
        return result.reverse().toString();
    }

	/**
	 * Decrypts a given string with the RSA keys and performs redundancy (CRC) check encoded in first two characters
	 * of the decrypted stream. When CRC fails null is returned, otherwise CRC characters are removed from the decrypted
	 * string.
	 *
	 * @param encrypted
	 * @param keys
	 * @return
	 */
	public String decryptWithRedundancyCheck( String encrypted, KeyPair keys ) {
		return redundancyCheck(
			decrypt( encrypted, keys )
		);
	}

	/**
	 * Performs redundancy (CRC) check encoded in first two characters of the decrypted stream.
	 * When CRC fails null is returned, otherwise CRC characters are removed from the decrypted string.
	 *
	 * @param decryptedData
	 * @return
	 */
	public String redundancyCheck(String decryptedData) {
		String redundancy = decryptedData.substring(0, 2);
		String original = decryptedData.substring(2);
		int check = hexToInt(redundancy);
		int sum = 0;
		for (int i = 0; i < original.length(); i++) {
			sum += (int)original.charAt(i);
		}
		if (check == (sum & 0xFF)) {
			return original;
		} else {
			return null;
		}
	}

    /**
     * Parse url string into the map. Performs URL decode with passed encoding settings.
	 * Native chars that doesn't get encoded on the client with encodeURI gets damaged during crypting phase.
	 *
     * @param url value to parse
     * @param encoding encoding value
     * @return Map with param name, value pairs
     */
    public static Map<String, Object> parse(String url,String encoding) {
        try {
            String urlToParse = URLDecoder.decode(url,encoding);
			return parseJCryptedData(urlToParse);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Unknown encoding.",e);
        }
    }

	/**
	 * Parses url string into the map of parameters. Map can contain only String or String[] depending on whether
	 * parameter is duplicated in input string or not.
	 *
	 * @param data
	 * @return
	 */
	public static Map<String, Object> parseJCryptedData(String data) {
		String[] params = SPLIT_BY_AMPERSAND.split(data);
		Map<String, Object> parsed = new HashMap<String, Object>();
		for(String param : params) {
			String[] p = SPLIT_BY_EQUALITY.split(param);
			String name = p.length > 0 ? p[0] : null;
			String value = p.length > 1 ? p[1] : "";
			if(name != null) {
				if (parsed.containsKey(name)) {
					Object existingValue = parsed.get(name);
					if (existingValue instanceof String[]) {
						String[] existingValues = (String[])existingValue;
						String[] combinedValues = new String[existingValues.length + 1];
						System.arraycopy(existingValues, 0, combinedValues, 0, existingValues.length);
						combinedValues[existingValues.length] = value;
						parsed.put(name, combinedValues);
					} else {
						parsed.put(name, new String[] {(String)existingValue, value});
					}
				} else {
					parsed.put(name, value);
				}
			}
		}
		return parsed;
	}

	/**
     * Return public RSA key modulus
     * @param keyPair RSA keys
     * @return modulus value as hex string
     */
    public static String getPublicKeyModulus( KeyPair keyPair ) {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        return publicKey.getModulus().toString(16);
    }

    /**
     * Return public RSA key exponent
     * @param keyPair RSA keys
     * @return public exponent value as hex string
     */
    public static String getPublicKeyExponent( KeyPair keyPair ) {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        return publicKey.getPublicExponent().toString(16);
    }

    /**
     * Max block size with given key length
     * @param keyLength length of key
     * @return numeber of digits
     */
    public static int getMaxDigits(int keyLength)   {
        return keyLength * 2 /16 +3;
    }

    /**
     * Convert byte array to hex string
     * @param bytes input byte array
     * @return Hex string representation
     */
    public static String byteArrayToHexString(byte[] bytes) {
		StringBuilder result = new StringBuilder();
		for(byte aByte : bytes) {
			result.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
		}
        return result.toString();
    }

    /**
     * Convert hex string to byte array
     * @param data input string data
     * @return bytes
     */
    public static byte[] hexStringToByteArray(String data) {
		int k = 0;
		byte[] results = new byte[data.length() / 2];
		for (int i = 0; i < data.length();) {
			results[k] = (byte) (Character.digit(data.charAt(i++), 16) << 4);
			results[k] += (byte) (Character.digit(data.charAt(i++), 16));
			k++;
		}
		return results;
	}

	/**
	 * Converts a hex string to int
	 *
	 * @param data
	 * @return
	 */
	public static int hexToInt(String data) {
		int result = 0;
		for (int i = 0; i < data.length(); i++) {
			result *= 16;
			if ((int)data.charAt(i) >= (int)'0' && (int)data.charAt(i) <= (int)'9') {
				result += (int)data.charAt(i) - (int)'0';
			} else if ((int)data.charAt(i) >= (int)'a' && (int)data.charAt(i) <= (int)'f') {
				result += 10 + (int)data.charAt(i) - (int)'a';
			} else if ((int)data.charAt(i) >= (int)'A' && (int)data.charAt(i) <= (int)'F') {
				result += 10 + (int)data.charAt(i) - (int)'A';
			}
		}
		return result;
	}

}